using System;
using System.Linq.Expressions;
using TelerikMvcGridCustomBindingHelper.Exceptions;
using TelerikMvcGridCustomBindingHelper.Util;

namespace TelerikMvcGridCustomBindingHelper.Mapper
{
    public interface IMappingExpression<TEntity, TViewModel> : IObsoleteMappingExpression<TEntity, TViewModel>
    {
        IMappingExpressionWithOptions<TEntity, TViewModel> MapProperty<TEntityProperty, TViewModelProperty>(Expression<Func<TEntity, TEntityProperty>> entityPropertySelector, Expression<Func<TViewModel, TViewModelProperty>> viewModelPropertySelector);

        IMappingExpressionWithOptions<TEntity, TViewModel> MapProperty<TEntityProperty, TViewModelProperty>(Expression<Func<TEntity, TEntityProperty>> entityPropertySelector, Expression<Func<TViewModel, TViewModelProperty>> viewModelPropertySelector, Action<IPropertyOptions<TEntity, TViewModel>> propertyOptions);
    }

    public interface IMappingExpressionWithOptions<TEntity, TViewModel> : IMappingExpression<TEntity, TViewModel>
    {
        IMappingExpressionWithOptions<TEntity, TViewModel> WithOptions(Action<IPropertyOptions<TEntity, TViewModel>> propertyOptions);
    }

    public interface IPropertyOptions<TEntity, TViewModel>
    {
        IPropertyOptions<TEntity, TViewModel> AcceptNullValuesWhenFiltering(bool acceptNullValuesWhenFiltering = true);

        IPropertyOptions<TEntity, TViewModel> SetValueResolver<TValueResolver>() where TValueResolver : class, IValueResolver, new();

        IPropertyOptions<TEntity, TViewModel> SetValueResolver(IValueResolver valueResolver);

        IPropertyOptions<TEntity, TViewModel> SetBooleanSubstitute<TBooleanSubstitute>() where TBooleanSubstitute : class, IBooleanSubstitute, new();

        IPropertyOptions<TEntity, TViewModel> SetBooleanSubstitute(IBooleanSubstitute booleanSubstitute);

        IPropertyOptions<TEntity, TViewModel> SetNullSubstitute<TNullSubstitute>() where TNullSubstitute : class, INullSubstitute, new();

        IPropertyOptions<TEntity, TViewModel> SetNullSubstitute(INullSubstitute nullSubstitute);

        IPropertyOptions<TEntity, TViewModel> SetNullAlias<TNullAlias>() where TNullAlias : class, INullAlias, new();

        IPropertyOptions<TEntity, TViewModel> SetNullAlias(INullAlias nullAlias);
    }

    public interface IObsoleteMappingExpression<TEntity, TViewModel>
    {
        [Obsolete("Use: MapProperty(...).WithOptions(options => options.SetValueResolver<SomeValueResolver>());")]
        IMappingExpression<TEntity, TViewModel> WithResolver<TValueResolver>() where TValueResolver : class, IValueResolver, new();

        [Obsolete("Use: MapProperty(...).WithOptions(options => options.SetBooleanSubstitute<SomeBooleanSubstitute>());")]
        IMappingExpression<TEntity, TViewModel> WithBooleanSubstitute<TBooleanSubstitute>() where TBooleanSubstitute : class, IBooleanSubstitute, new();

        [Obsolete("Use: MapProperty(...).WithOptions(options => options.SetBooleanSubstitute<SomeBooleanSubstitute>());")]
        IMappingExpression<TEntity, TViewModel> WithBooleanSubstitute(IBooleanSubstitute booleanSubstitute);
    }

    class PropertyOptions<TEntity, TViewModel> : IPropertyOptions<TEntity, TViewModel>
    {
        public bool ShouldAcceptNullValuesWhenFiltering { get; private set; }
        public IValueResolver ValueResolver { get; private set; }
        public IBooleanSubstitute BooleanSubstitute { get; private set; }
        public INullSubstitute NullSubstitute { get; private set; }
        public INullAlias NullAlias { get; private set; }

        public bool HasValueResolver()
        {
            return ValueResolver != null;
        }

        public bool HasBooleanSubstitute()
        {
            return BooleanSubstitute != null;
        }

        public bool HasNullSubstitute()
        {
            return NullSubstitute != null;
        }

        public bool HasNullAlias()
        {
            return NullAlias != null;
        }

        #region Implementation of IPropertyOptions<TEntity,TViewModel>

        public IPropertyOptions<TEntity, TViewModel> AcceptNullValuesWhenFiltering(bool acceptNullValuesWhenFiltering = true)
        {
            ShouldAcceptNullValuesWhenFiltering = acceptNullValuesWhenFiltering;
            return this;
        }

        public IPropertyOptions<TEntity, TViewModel> SetValueResolver<TValueResolver>() where TValueResolver : class, IValueResolver, new()
        {
            return SetValueResolver(ObjectFactory<TValueResolver>.Create());
        }

        public IPropertyOptions<TEntity, TViewModel> SetValueResolver(IValueResolver valueResolver)
        {
            ValueResolver = valueResolver;
            return this;
        }

        public IPropertyOptions<TEntity, TViewModel> SetBooleanSubstitute<TBooleanSubstitute>() where TBooleanSubstitute : class, IBooleanSubstitute, new()
        {
            return SetBooleanSubstitute(ObjectFactory<TBooleanSubstitute>.Create());
        }

        public IPropertyOptions<TEntity, TViewModel> SetBooleanSubstitute(IBooleanSubstitute booleanSubstitute)
        {
            BooleanSubstitute = booleanSubstitute;
            return this;
        }

        public IPropertyOptions<TEntity, TViewModel> SetNullSubstitute<TNullSubstitute>() where TNullSubstitute : class, INullSubstitute, new()
        {
            return SetNullSubstitute(ObjectFactory<TNullSubstitute>.Create());
        }

        public IPropertyOptions<TEntity, TViewModel> SetNullSubstitute(INullSubstitute nullSubstitute)
        {
            NullSubstitute = nullSubstitute;
            return this;
        }

        public IPropertyOptions<TEntity, TViewModel> SetNullAlias<TNullAlias>() where TNullAlias : class, INullAlias, new()
        {
            return SetNullAlias(ObjectFactory<TNullAlias>.Create());
        }

        public IPropertyOptions<TEntity, TViewModel> SetNullAlias(INullAlias nullAlias)
        {
            NullAlias = nullAlias;
            return this;
        }

        #endregion
    }

    class MappingExpression<TEntity, TViewModel> : IMappingExpressionWithOptions<TEntity, TViewModel>
    {
        private readonly GridPropertyInfo _gridPropertyInfo;

        public MappingExpression(GridPropertyInfo gridPropertyInfo)
        {
            _gridPropertyInfo = gridPropertyInfo;
        }

        public IMappingExpressionWithOptions<TEntity, TViewModel> MapProperty<TEntityProperty, TViewModelProperty>(Expression<Func<TEntity, TEntityProperty>> entityPropertySelector, Expression<Func<TViewModel, TViewModelProperty>> viewModelPropertySelector)
        {
            return GridModelMapperBase<TEntity, TViewModel>.MapProperty(entityPropertySelector, viewModelPropertySelector);
        }

        public IMappingExpressionWithOptions<TEntity, TViewModel> MapProperty<TEntityProperty, TViewModelProperty>(Expression<Func<TEntity, TEntityProperty>> entityPropertySelector, Expression<Func<TViewModel, TViewModelProperty>> viewModelPropertySelector, Action<IPropertyOptions<TEntity, TViewModel>> propertyOptions)
        {
            return MapProperty(entityPropertySelector, viewModelPropertySelector).WithOptions(propertyOptions);
        }

        public IMappingExpressionWithOptions<TEntity, TViewModel> WithOptions(Action<IPropertyOptions<TEntity, TViewModel>> propertyOptions)
        {
            Guard.If(_gridPropertyInfo == null)
                .Throw<NotSupportedException>(string.Format("Could not find the target property."));

            // ReSharper disable PossibleNullReferenceException
            //todo: well, this should be supported, probably in a future version...
            Guard.If(_gridPropertyInfo.CustomExpression != null)
                .Throw<NotSupportedException>(string.Format("TEntity CustomExpression's is not compatible with options. Failed expression: {0}", _gridPropertyInfo.CustomExpression));
            // ReSharper restore PossibleNullReferenceException

            var options = new PropertyOptions<TEntity, TViewModel>();

            propertyOptions(options);

            if (options.HasValueResolver())
            {
                SetValueResolver(options.ValueResolver);
            }

            if (options.HasBooleanSubstitute())
            {
                SetBooleanSubstitute(options.BooleanSubstitute, false);
            }

            if (options.HasNullSubstitute())
            {
                SetNullSubstitute(options.NullSubstitute, false);
            }

            if (options.HasNullAlias())
            {
                SetNullAlias(options.NullAlias, false);
            }

            if (options.ShouldAcceptNullValuesWhenFiltering)
            {
                // ReSharper disable PossibleNullReferenceException
                _gridPropertyInfo.AcceptNullValuesWhenFiltering = true;
                // ReSharper restore PossibleNullReferenceException
            }

            return this;
        }

        #region ValueResolvers

        internal void AddGlobalValueResolver(IGlobalValueResolver globalValueResolver)
        {
            SetValueResolver(globalValueResolver);
        }

        IMappingExpression<TEntity, TViewModel> IObsoleteMappingExpression<TEntity, TViewModel>.WithResolver<TValueResolver>()
        {
            return SetValueResolver(ObjectFactory<TValueResolver>.Create());
        }

        private IMappingExpression<TEntity, TViewModel> SetValueResolver(IValueResolver valueResolver)
        {
            var isGlobal = valueResolver is IGlobalValueResolver;

            Guard.If(isGlobal == false && _gridPropertyInfo == null)
                .Throw<ValueResolverRegistrationException>(valueResolver,
                    string.Format("Could not find the target property for the ValueResolver: {0}", valueResolver));

            var valueResolverKey = isGlobal
                ? ValueResolverKey.GlobalKey(((IGlobalValueResolver)valueResolver).GetSourceType(), ((IGlobalValueResolver)valueResolver).GetDestinationType())
                : ValueResolverKey.New(typeof(TEntity), typeof(TViewModel), _gridPropertyInfo);

            var containsKey = GridModelMapper.ConfigurationObject.ValueResolvers.ContainsKey(valueResolverKey);

            while (containsKey)
            {
                IValueResolver vr;
                containsKey = GridModelMapper.ConfigurationObject.ValueResolvers.TryRemove(valueResolverKey, out vr);
            }

            var tryAdd = GridModelMapper.ConfigurationObject.ValueResolvers.TryAdd(valueResolverKey, valueResolver);

            Guard.If(tryAdd == false).Throw<ValueResolverRegistrationException>(valueResolver);

            return this;
        }

        #endregion

        #region BooleanSubstitutes

        internal void SetGlobalBooleanSubstitute(IBooleanSubstitute booleanSubstitute)
        {
            SetBooleanSubstitute(booleanSubstitute, true);
        }

        IMappingExpression<TEntity, TViewModel> IObsoleteMappingExpression<TEntity, TViewModel>.WithBooleanSubstitute<TBooleanSubstitute>()
        {
            return SetBooleanSubstitute(ObjectFactory<TBooleanSubstitute>.Create(), false);
        }

        IMappingExpression<TEntity, TViewModel> IObsoleteMappingExpression<TEntity, TViewModel>.WithBooleanSubstitute(IBooleanSubstitute booleanSubstitute)
        {
            return SetBooleanSubstitute(booleanSubstitute, false);
        }

        private IMappingExpression<TEntity, TViewModel> SetBooleanSubstitute(IBooleanSubstitute booleanSubstitute, bool isGlobal)
        {
            Guard.If(isGlobal == false && _gridPropertyInfo == null)
                .Throw<BooleanSubstituteRegistrationException>(booleanSubstitute,
                    string.Format("Could not find the target property for the BooleanSubstitute: {0}", booleanSubstitute));

            var booleanSubstituteKey = isGlobal
                ? BooleanSubstituteKey.GlobalKey()
                : BooleanSubstituteKey.New(typeof(TEntity), typeof(TViewModel), _gridPropertyInfo);

            var containsKey = GridModelMapper.ConfigurationObject.BooleanSubstitutes.ContainsKey(booleanSubstituteKey);

            while (containsKey)
            {
                IBooleanSubstitute bs;
                containsKey = GridModelMapper.ConfigurationObject.BooleanSubstitutes.TryRemove(booleanSubstituteKey, out bs);
            }

            var tryAdd = GridModelMapper.ConfigurationObject.BooleanSubstitutes.TryAdd(booleanSubstituteKey, booleanSubstitute);

            Guard.If(tryAdd == false).Throw<BooleanSubstituteRegistrationException>(booleanSubstitute);

            return this;
        }

        #endregion

        #region NullSubstitutes

        internal void SetGlobalNullSubstitute(INullSubstitute nullSubstitute)
        {
            SetNullSubstitute(nullSubstitute, true);
        }

        private void SetNullSubstitute(INullSubstitute nullSubstitute, bool isGlobal)
        {
            Guard.If(isGlobal == false && _gridPropertyInfo == null)
                .Throw<NullSubstituteRegistrationException>(nullSubstitute,
                    string.Format("Could not find the target property for the NullSubstitute: {0}", nullSubstitute));

            var nullSubstituteKey = isGlobal
                ? NullSubstituteKey.GlobalKey()
                : NullSubstituteKey.New(typeof(TEntity), typeof(TViewModel), _gridPropertyInfo);

            var containsKey = GridModelMapper.ConfigurationObject.NullSubstitutes.ContainsKey(nullSubstituteKey);

            while (containsKey)
            {
                INullSubstitute ns;
                containsKey = GridModelMapper.ConfigurationObject.NullSubstitutes.TryRemove(nullSubstituteKey, out ns);
            }

            var tryAdd = GridModelMapper.ConfigurationObject.NullSubstitutes.TryAdd(nullSubstituteKey, nullSubstitute);

            Guard.If(tryAdd == false).Throw<NullSubstituteRegistrationException>(nullSubstitute);
        }

        #endregion

        #region NullAlias

        internal void SetGlobalNullAlias(INullAlias nullAlias)
        {
            SetNullAlias(nullAlias, true);
        }

        private void SetNullAlias(INullAlias nullAlias, bool isGlobal)
        {
            Guard.If(isGlobal == false && _gridPropertyInfo == null)
                .Throw<NullAliasRegistrationException>(nullAlias,
                    string.Format("Could not find the target property for the NullAlias: {0}", nullAlias));

            var nullAliasKey = isGlobal
                ? NullAliasKey.GlobalKey()
                : NullAliasKey.New(typeof(TEntity), typeof(TViewModel), _gridPropertyInfo);

            var containsKey = GridModelMapper.ConfigurationObject.NullAliases.ContainsKey(nullAliasKey);

            while (containsKey)
            {
                INullAlias na;
                containsKey = GridModelMapper.ConfigurationObject.NullAliases.TryRemove(nullAliasKey, out na);
            }

            var tryAdd = GridModelMapper.ConfigurationObject.NullAliases.TryAdd(nullAliasKey, nullAlias);

            Guard.If(tryAdd == false).Throw<NullAliasRegistrationException>(nullAlias);
        }

        #endregion
    }
}